from JDjango.api import model_type_data
from ..common import *
from .. import config as SCONFIGS

__all__ = [
    'get_models_path_by_appname', # 获取当前app下所有的模型文件路径
    'get_models_by_appname', # 获取当前app下的所有模型
    'get_models_from_modelspy', # 从模型文件中读取所有模型
    'get_all_models_link_app_path', # 获取所有的模型，以及其关联的应用程序和路径
    'get_fields_by_modelname', # 通过模型名称，获取该模型下的所有字段集合
    'get_meta_exist_options', # 获取 Meta 的参数选项集
    'get_location_from_modelspy', # 定位模型在文件中的位置
    'cut_model_content_from_file', # 从模型文件中，抠出指定的模型代码段（优化版，仍然存在隐患）
    'cut_model_Meta_content_from_file', # 从模型文件中，抠出Meta选项代码段
]

def get_models_path_by_appname(appname: str)->List[str]:
    """获取当前app下所有的模型文件路径
    
        规则：从当前app开始递归式遍历
    """
    APP_PATH = os.path.join(djangotools.SCONFIGS.dirname(), *appname.split('.')) # 路径定位到当前app下
    models_path = [] # 只要是可能的模型文件全部读取
    if os.path.exists(APP_PATH) and os.path.isdir(APP_PATH):
        pys = glob.glob(os.path.join(APP_PATH, '**', '*.py'), recursive=True) # 先取所有归属当前app下的文件路径
        alias = env.get_file_alias('models') # 取所有模型别名（如：models.py）
        models_path.extend([_ for _ in pys if os.path.basename(_) in alias]) # 以别名为依据，过滤所有文件中可能的模型文件
    return models_path

def get_models_from_modelspy(path):
    """从模型文件中读取所有自定义模型名称"""
    result = []
    for _ in read_file_list(path):
        result.extend(retools.PATT_MODEL.findall(_.strip()))
    return result


def get_models_by_appname(appname: str)->List[str]:
    """获取当前app下的所有模型
    
        给定 appname ，返回该 appname 下的所有模型名称
    """
    pathModels = get_models_path_by_appname(appname)
    data = []
    for path in pathModels:
        data.extend(get_models_from_modelspy(path))
    return data


def get_all_models_link_app_path()->List[Any]:
    '''获取所有的模型，以及其关联的应用程序和路径
    
        返回三元组 (应用程序名, [模型名称集合], 模型归属路径)
    '''
    datas = []
    app_names = djangotools.SCONFIGS.app_names()
    for app_name in app_names:
        model_paths = get_models_path_by_appname(app_name)
        for path in model_paths:
            datas.append((app_name, get_models_from_modelspy(path), path))
    return datas

'''
    解析模型，解析字段选项，返回模型解析数据包
'''
def _get_field_name(line: str)->str:
    '''尝试从一行文本中获取字段的代码名。
    
        如 name = models.CharField(...) 将获取到 'name'
        调用此方法前，请确定当前行是注册过的字段行
        无匹配项返回 None
    '''
    temp = [_.strip() for _ in line.split('=') if _.strip()]
    if len(temp) > 0:
        p = retools.PATT_VAR_NAME_RULE.match(temp[0])
        if p:
            return p.group(0)
    return None

def _clearfirst_field_lines(field_lines: List[str], types: List[str])->Dict[str, Any]:
    '''清洗整合字段代码，返回每一个字段的选项集合【初步获取，返回的结果集需要深度解析】
    
        field_lines 传递的是末尾没有换行符的拆行集合
        本方法不对 options 做填充，将直接赋 'options': []

        数据包结构：
        {
            'demo_field_code_name': {
                'ori_text': [
                    "",
                    "",
                    ...
                ],
                'options': {
                    'demo_option_name': '',
                },
                'type_name': 'CharField'
            }
        }
    '''
    fields = {}
    lastest_field_name = '' # 最新查找到的字段
    for line in field_lines: # 自上而下解析
        matchs = [_ for _ in retools.PATT_MAYBE_FIELD_TYPE_NAME.findall(line) if _ in types]
        if len(matchs) > 0: # 理论上只有一个，但允许有 > 1 的容错【若大于1，则优先取第一个匹配的类型】
            match_type_name = matchs[0] # 取第一个
            field_name = _get_field_name(line) # 尝试查找模型代码名称
            if field_name is None:
                if lastest_field_name:
                    fields[lastest_field_name]['ori_text'].append(line)
                continue # 此行归属上方最邻近的字段
            ### 正确的情况下，不可能出现两个字段代码名重复的情况，因为这没有意义
            fields[field_name] = {'ori_text':[line,], 'options':{}, 'type_name':match_type_name}
            lastest_field_name = field_name
        else: # 不匹配默认归属最临近的上一行【此处为拆行合并的功能】
            if '' != lastest_field_name:
                fields[lastest_field_name]['ori_text'].append(line)
    return fields

def _judge_keywargs(arg_str: str)->Tuple[bool, str, str]:
    '''如果是关键字参数返回 True，否则返回 False
    
        [是否关键字参数, 键，值]
    '''
    split_arg_str = arg_str.split('=')
    temp_str = split_arg_str[0].strip() # 键名
    if retools.PATT_VAR_NAME_RULE.match(temp_str):
        return True, temp_str, arg_str[arg_str.find('=')+1:].strip()
    else:
        return False, temp_str, None # 位置参数，默认是 verbose_name

def _analysis_options_fields(clear_lines: Dict[str, Any], options):
    """对 _clearfirst_field_lines 解析出来的数据包再次深度解析处理"""
    '''
        将数据包里的 ori_text 解析为 options
    '''
    for k, v in clear_lines.items():
        ori_text: List[str] = ' '.join(v['ori_text']) # 解析
        op_options: Dict[str, Any] = v['options'] # 插入
        ### 括号匹配算法截取字段的参数区域
        cut_ori_text = cut_content_by_doublecode(ori_text, leftCode='(', rightCode=')')
        # cut_ori_text = "'hello', k=func(1,(2,),i=3), w = 9"
        split_args_list = split_args_by_sep(cut_ori_text)
        '''
            关键字参数 和 位置参数的判断方式
            关键字参数： '=' 拆分，左边符合变量名即是关键字参数，否则是位置参数
        '''
        flag = 0
        for _ in split_args_list:
            judge = _judge_keywargs(_)
            if judge[0]:
                op_options[judge[1]] = judge[2]
                if "verbose_name" == judge[1]: flag = 1
            else:
                op_options['verbose_name'] = judge[1]
                flag = 1
        
        if not cut_ori_text or 0 == flag: # 无参数，或未赋值过 verbose_name
            op_options['verbose_name'] = ori_text.split("=")[0].strip()

def get_fields_by_modelname(app_name: str, model_name: str)->Dict[str, Any]:
    '''通过模型名称，获取该模型下的所有字段集合，返回一个字段集合数据包
    
        不仅仅只有字段名称，包括字段的所有选项，均被读取
        暂不支持 末尾为 \ 的Python拆行语法

        如：
        class DemoModel(models.Model):
            name = models.CharField('姓名', max_length=255)

        将被解析为：
        {
            'name': {
                'ori_text': [
                    "name = models.CharField('姓名', max_length=255)",
                ],
                'options': {
                    'verbose_name': '姓名',
                    'max_length': '255',
                },
                'type_name': 'CharField'
            }
        }
    '''
    pathModels = get_models_path_by_appname(app_name) # 获取某一应用程序下的所有可能的模型文件路径
    # patt_model = re.compile(r'class\s+' + model_name + r'\s*\(\s*[a-zA-Z0-9]*?[.]*?Model\s*\):')
    for path in pathModels:
        whole_content = cut_model_content_from_file(path, model_name) # 截取的完整模型区域
        types = model_type_data.get_all_field_types()
        options = model_type_data.get_all_field_options()
        t = _clearfirst_field_lines(whole_content.split('\n'), types) # 数据包的初步构建
        _analysis_options_fields(t, options) # 二次处理
        return t

        '''
            初代 代码【暂存】
        '''
        # whole_content = re_file_list_del_comment_3point(path) # 读整个文件，并删掉所有的注释
        # search = patt_model.search(whole_content)
        # if search:
        #     match_text = search.group(0)
        #     # 定位到模型开始的那一行（往后截取）
        #     cut_content = whole_content[whole_content.find(match_text)+len(match_text):]
        #     # 缩进截取法【不考虑拆行的情况，即末尾为右斜杠】后面大众化后可能会补全此功能
        #     # 以缩进为依据，截取属于模型的部分（正常是以无缩进行的那一行为终止行）
        #     field_lines = [] # 可能的字段行字符集，需要二次清洗
        #     for line in (_ for _ in cut_content.split('\n')):
        #         if line and line[0] not in string.whitespace: # 此判断可增加或修改判定模型结束的条件，目前设置的比较简单
        #             break
        #         field_lines.append(line)
        #     '''
        #         正式开始解析
        #     '''
        #     types = model_type_data.get_all_field_types()
        #     options = model_type_data.get_all_field_options()
        #     t = _clearfirst_field_lines(field_lines, types)
        #     _analysis_options_fields(t, options) # 二次处理
        #     return t
    else:
        return {}


def get_location_from_modelspy(app_name: str, model_name: str, model_path: str=None):
    """定位 模型 在 模型文件 中的位置【仅在应用程序根路径下搜索】
    
        app_name: 应用程序名称
        model_name: 模型名称
        【匹配即返回的原则】
        (行, 列, 模型路径)
    """
    
    if "" == model_name:
        return (1, 0, None) # 默认第一行，第一个字符
    patt_model = re.compile(r'class\s+' + model_name + r'\s*\(\s*[a-zA-Z0-9]*?[.]*?Model\s*\):')

    if model_path is not None:
        '''如果直接传入路径的话，就直接处理【不需要再判断，可以直接用，绝对正确】'''
        for i, line in enumerate(read_file_list_del_comment(model_path)):
            if patt_model.search(line):
                return (i+1, 0, model_path)
        return (1, 0, None)
        
    model_alias = env.get_file_alias('models') # 获取模型所有的别名
    app_names: list[str] = djangotools.get_appnames_by_dirname(djangotools.SCONFIGS.dirname()) # 获取所有可能的应用程序名

    # 从 app_names 中匹配与 app_name 最匹配的一个（用非标准匹配标准，防止解析错误）
    '''
        比如 app_name 传入的是 myapp，但是实际能够匹配的是 apps.myapp
        这时候要用 myapp 找到 apps.myapp
    '''
    judge_split_name = app_name.split('.')
    for temp_name in app_names:
        if temp_name.split('.')[-1] in judge_split_name:
            app_name = temp_name # 找到实际可用的标准的应用程序名
            break

    '''
        直接递归式查找
    '''
    common_path = os.path.join(SCONFIGS.dirname(), *app_name.split('.')) # 默认路径
    search_path = os.path.join(common_path, '**', '*.py')
    objs = glob.glob(search_path, recursive=True)
    for obj in objs:
        for _ in model_alias:
            if os.path.isfile(obj) and os.path.basename(obj) == _:
                for i, line in enumerate(read_file_list_del_comment(obj)):
                    if patt_model.search(line):
                        return (i+1, 0, obj)

    return (1, 0, None)

def cut_model_content_from_file(model_path: str, model_name: str, del_comment=False)->str:
    """从模型文件中，抠出指定的模型代码段
    
        扣取原则：
        从疑似模型的那一行开始，向后截取
        一旦遇到 def、class 开头的代码则停止搜索
        从停止搜索的那一行往上，删除#注释、三引号注释、@修饰符

        剩下的内容则是该模型的所有截取内容。

        日后存在的隐患：多行文本某一行以 def 或 class 开头的话，会影响解析引擎，所以在使用时应该避免使用多行文本

        del_comment: 表示是否开启删除注释，默认不删除【此功能为预置功能，尚未实现】
    """
    row, col, _ = get_location_from_modelspy('', model_name, model_path)
    content_list = read_file_list(model_path)[row-1:]
    if len(content_list) <= 0:
        return ''
    model_content: List[str] = [content_list[0]]
    for content in content_list[1:]: # 跳过第一行，第一行肯定符合退出条件
        if retools.PATT_FUNC.match(content) or retools.PATT_CLASS.match(content):
            break
        model_content.append(content)
    '''
        倒序删除注释和修饰器（删除一定是连续的，若不连续，直接退出删除操作）

        多行文本删除控制规则：
            当行构成多行文本的语法，当全部删除
            当行不构成完整的多行文本，则判断最后是否是三引号，往上找以三引号开头的，即构成一个完整的多行文本语法
    '''
    flag = 0 # 多行文本删除控制
    count = 0 # 判断连续的辅助字段
    len_model_content = len(model_content) - 1
    for i, _ in enumerate(model_content[::-1]):
        if count != i:
            break # 删除操作不连续时退出（表示删除操作结束，剩下的就是模型的相关代码）
        if 0 == flag:
            # 优先删除 # 注释和 @ 修饰符
            if retools.PATT_COMMENT.match(_):
                count += 1
                model_content.pop(len_model_content - i)
                continue
            if retools.PATT_DECORATE.match(_):
                model_content.pop(len_model_content - i)
                count += 1
                continue
            # 剩下的就是多行文本语法
            remain_content = retools.PATT_COMMENT_3POINT.sub('', _).strip() # 先去除影响项（同时去除首位空白符）
            if not remain_content: # 为空表示纯注释，删除
                model_content.pop(len_model_content - i)
                count += 1
                continue

            # 检测去除影响项后还是不是三引号结尾的
            if remain_content[-3:] in ('"""', "'''"):
                flag = 1 # 开始删除
                model_content.pop(len_model_content - i)
                count += 1
                continue
        else:
            model_content.pop(len_model_content - i)
            count += 1
            if _.strip()[:3] in ('"""', "'''"):
                flag = 0 # 标志着一个多行文本的结束
            continue
    return ''.join(model_content)

def cut_model_Meta_content_from_file(model_path: str, model_name: str, del_comment=False)->str:
    """从模型文件中，抠出Meta选项代码段
    
        扣取原则：
        先扣模型代码段（确保完整性和准确性）
        后定位Meta：Meta下方遇到def为止，如果没有def就是全部。

        找不到返回空字符串。
    """
    model_content = cut_model_content_from_file(model_path, model_name, del_comment)
    if retools.PATT_META.search(model_content):
        flag_meta = 0 # 0表示没有找到Meta，1表示找到Meta
        meta_contents: List[str] = [] # 包含 class Meta: 这一行 
        for line in model_content.split('\n'):
            if 0 == flag_meta and retools.PATT_META.search(line):
                '''定位到 Meta 那一行'''
                flag_meta = 1 # 从下一行开始截取
            if 1 == flag_meta:
                if retools.PATT_FUNC.match(line) or retools.PATT_CLASS.match(line):
                    break
                else:
                    meta_contents.append(line)
        return '\n'.join(meta_contents)
    else:
        return ''

def get_meta_exist_options(model_path: str, model_name: str, del_comment=False):
    '''获取 Meta 的参数选项集
    
        检测是否是选项的依据:
            = 拆分，第一个索引值在选项列表里即可。

        数据结构：
        {
            'key': 'value'
        }
    '''
    return_options: Dict[str, Any] = {}
    meta_options = model_type_data.read_meta_options()
    options = model_type_data.get_all_meta_options() # 标准的Meta参数名称集合
    meta_content = cut_model_Meta_content_from_file(model_path, model_name, del_comment)
    for line in meta_content.split('\n')[1:]: # 直接从参数行开始解析
        try: # 目前仅处理非拆分行的情况（属于拆分行的参数暂时跳过）
            temp_option = line.split('=')[0].strip()
            if temp_option in options:
                return_options[temp_option] = eval(('='.join(line.split('=')[1:])).strip())
        except:
            ...
    return return_options
